/*
 * Player.java
 *
 * Copyright (c) 2010 Jaymie Strecker (strecker at cs.umd.edu). All rights reserved.
 *
 * This file is part of Tonic.
 *
 * Tonic is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Tonic is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Tonic.  If not, see <http://www.gnu.org/licenses/>.
 */
package tonic.audio;

import tonic.error.InternalException;
import java.io.*;
import java.util.ArrayList;
import javax.sound.sampled.*;
import tonic.gui.ErrorDisplay;

/**
 * Player provides methods for playing and pausing an audio and marking excerpts in it.
 * It has three modes: no file open, playing, and paused.
 *
 * If no file is open, then most methods in this class are invalid. 
 *
 * @author Jaymie Strecker (strecker at cs.umd.edu)
 */
public class Player {
    private static File audioFile = null;
    private static AudioData audioData = null;
    private static SourceDataLine audioLine = null;
    private static AudioInputStream audioStream = null;
    private static int sampleAdjustment = 0;
    private static int markStart = 0;
    private static PlayerFeed playerFeed = null;
    private static volatile boolean isPlaying = false;

    /**
     * Player cannot be instantiated. 
     */
    private Player() {
        // nop
    }

    /**
     * Accesses the current audio file, or returns null if in no-file-open mode. 
     */
    public static File getFile() {
        return audioFile;
    }

    /**
     * Sets the current audio file to the given file. The previous file's excerpts are
     * discarded. The Player is put into paused mode.
     *
     * @param file  the audio file to open
     * @throws IOException  if there is a problem closing the previous audio file or
     *                      accessing the current one
     */
    public static void setFile(File file) throws IOException {
        isPlaying = false;
        closeAudio();

        audioFile = file;
        audioData = new AudioData(audioFile);
        openAudio();

        sampleAdjustment = 0;
        markStart = 0;
        playerFeed = null;
    }

    /**
     * Opens audioStream and audioLine in preparation for playing.
     *
     * @throws IOException  if there is a problem accessing the current audio file
     */
    private static void openAudio() throws IOException {
        try {
            audioStream = AudioSystem.getAudioInputStream(audioFile);
            AudioFormat format = audioStream.getFormat();
            audioLine = AudioSystem.getSourceDataLine(format);
            audioLine.open(format);
        } catch (UnsupportedAudioFileException e) {
            throw new IOException(e);
        } catch (LineUnavailableException e) {
            throw new IOException(e);
        }
    }

    /**
     * If audioStream and/or audioLine are open, closes them. Otherwise does nothing.
     *
     * @throws IOException  if there is a problem closing audioStream or audioLine
     */
    private static void closeAudio() throws IOException {
        if (audioLine != null) {
            audioLine.close();
        }
        if (audioStream != null) {
            audioStream.close();
        }
    }

    /**
     * If the Player is playing, accesses the thread that causes the audio to play.
     * Otherwise returns null. 
     */
    public static Thread getPlayThread() {
        // TODO synchronize access to isPlaying
        if (isPlaying) {
            return playerFeed;
        } else {
            return null; 
        }
    }

    /**
     * Thread that causes the audio to play. 
     */
    private static class PlayerFeed extends Thread {

        /**
         * Feeds data from audioStream to audioLine either until the data is exhausted or
         * until the Player goes into paused mode. 
         */
        @Override
        public void run() {
            try {
                int bufferSize = audioLine.getBufferSize();
                while (isPlaying) {
                    byte[] buffer = new byte[bufferSize];
                    int numBytesRead = audioStream.read(buffer, 0, bufferSize);
                    if (numBytesRead == -1) {
                        audioLine.drain();
                        pause();
                    } else {
                        audioLine.write(buffer, 0, numBytesRead);
                    }
                }
            } catch (Exception e) {     // IOException, InternalException
                // Can't throw the exception because method overrides Thread.run
                new ErrorDisplay(null, "There was a problem playing the audio file.", e);
            }
        }
    }

    /**
     * If paused, starts the audio file playing (and changes the mode accordingly).
     * If already playing, does nothing. 
     */
    public static void play() {
        // TODO synchronize access to isPlaying
        if (! isPlaying) {
            isPlaying = true;
            audioLine.start();
            playerFeed = new PlayerFeed();
            playerFeed.start();
        }
    }

    /**
     * If playing, pauses the audio file (and changes the mode accordingly). If already
     * paused, does nothing.
     *
     * @throws IOException  if there is a problem accessing the audio file
     * @throws InternalException    if the player is in an invalid state or there is a
                                    problem analyzing the excerpts
     */
    public static void pause() throws InternalException, IOException {
        // TODO - Sometimes in the last 1-2 sec. of an audio file, if you pause and then,
        // without scrolling, try to play again, you have to press play a second time.
        // Presumably this is when the pause occurs during the call to audioLine.drain().

        // TODO synchronize access to isPlaying
        if (isPlaying) {
            isPlaying = false;
            audioLine.stop();
            audioLine.flush();
            setSamplePosition(getSamplePosition());     // rewind past the flushed bytes
            audioData.analyzeExcerpts(); 
        }
    }

    /**
     * Accesses the current position (index into the audio file's samples) at which the
     * audio is playing or paused. 
     */
    public static int getSamplePosition() {
        return audioLine.getFramePosition() + sampleAdjustment;
    }

    /**
     * If paused, sets the current audio position (index into the audio file's samples)
     * to the given index. 
     *
     * @param sampleNum     the index into the audio file's samples at which to set the position
     * @throws IOException  if there is a problem accessing the audio file
     * @throws InternalException    if the player is playing
     */
    public static void setSamplePosition(int sampleNum) throws InternalException, IOException {
        if (isPlaying) {
            throw new InternalException("Can't set audio position while playing");
        }
        closeAudio();
        openAudio();
        sampleAdjustment = sampleNum;
        int bytePosition = sampleNum * audioStream.getFormat().getFrameSize();
        audioStream.skip(bytePosition);
    }

    /**
     * Marks the start of an excerpt at the current audio position. The excerpt is not
     * created until stopMark is called. 
     */
    public static void startMark() throws InternalException {
        markStart = getSamplePosition();
    }

    /**
     * Marks the end of an excerpt at the current audio position, and creates the excerpt.
     */
    public static void stopMark() {
        int markEnd = getSamplePosition();
        audioData.addExcerpt(markStart, markEnd); 
    }

    /**
     * Returns the excerpt at the given index for the current audio file. 
     *
     * @param index     index into the list of excerpts
     */
    public static Excerpt getExcerpt(int index) {
        return audioData.getExcerpt(index);
    }

    /**
     * Returns the number of excerpts created so far for the current audio file. 
     */
    public static int getNumExcerpts() {
        return audioData.getNumExcerpts();
    }

    /**
     * Sets the excerpt list for the current audio file.
     *
     * @param excerpts  the excerpt list - assumed to be valid
     */
    protected static void setExcerpts(ArrayList<Excerpt> excerpts) {
        audioData.setExcerpts(excerpts);
    }

    /**
     * Returns the sample at the given index in the current audio file.
     *
     * @param index     the index into the samples
     */
    public static float getSample(int index) {
        return audioData.getSample(index); 
    }

    /**
     * Returns the sample rate (samples/sec.) in the current audio file. 
     */
    public static float getSamplesPerSecond() {
        return audioData.getSamplesPerSecond();
    }

    /**
     * Returns the number of samples in the current audio file. 
     */
    public static int getNumSamples() {
        return audioData.getNumSamples();
    }

    /**
     * Returns the position at which the most recently marked excerpt begins (this may
     * be an excerpt currently being marked and not yet added to the list of excerpts),
     * or 0 if no excerpts have been marked yet.
     */
    public static int getMarkStart() {
        return markStart;
    }

    /**
    public static void main(String[] args) throws Exception {
        File file = new File("test_inputs/flute - note - Bb4.wav");
//        File file = new File("test_inputs/band - music - Thick As A Brick 1.wav");
        Player p = new Player();
        p.setFile(file);
        p.play();
    }
     * 
     */
}

